Serverless Dashboardを使って爆速でCICD環境をセットアップ #pytest
Serverless Frameworkのダッシュボードがあるのを最近知って使ってみました。
かなり使い心地が良くて画面上からサクッとCICD環境を作成できたので、この記事で手順を残しておきます。
今回はデプロイ先はAWSでブランチ毎にAWSアカウントを切り替える、ランタイムはPythonでpytestをデプロイ前に実行する、という構成にしていきます。
環境
- Serverless Framework
- Framework Core: 2.2.0
- Plugin: 4.0.4
- SDK: 2.3.2
- Components: 3.1.4
- Python: 3.8.5
- Pipenv: 2020.8.13
- pytest: 6.0.2
- moto: 1.3.16
セットアップ
serverlessが入っていない場合はインストール
$ npm i -g serverless
プロジェクトを用意
$ mkdir sample-sls-cicd $ cd sample-sls-cicd
ここではpipenvを使ってPython環境をセットアップします。(Pythonの環境構築等の説明は割愛します)
$ pipenv --python 3.8.5 $ pipenv shell $ pipenv install -d autopep8 pylint pytest moto
Serverless Frameworkにサインアップ
Serverless Dashboardにサインアップします。
CICDはProプランの機能になりますが、最初は無料利用枠で試せます。
アカウントはGithub, Gmail, Eメールのいずれかで登録できます。
(任意)orgを作成
Serverless Frameworkではorgという単位でプロジェクトを管理できます。1つのorgの中に複数のappを作成して デフォルトでユーザー名と同一のorgが作成されているので任意のorgにアプリケーションを所属させたい場合はorgを事前に作成してください。
CLIからログイン
アカウントを作成したらCLIを認証します。
プロジェクトルートに serverless.yml
を用意します。
service: name: sample-sls-cicd provider: name: aws region: ap-northeast-1
一旦最低限の記述でCLIから認証します。
$ sls login Serverless: Logging you in via your default browser... Serverless: You sucessfully logged in to Serverless.
ブラウザが開くので、作成したアカウントでログインできていることを確認します。
serverlessコマンドを実行して、orgに対してアプリケーションを追加します。
$ sls You can monitor, troubleshoot, and test your new service with a free Serverless account. Serverless: Would you like to enable this? Yes Serverless: What org do you want to add this to? experiment (orgを選択) Serverless: What do you want to name this application? sample-sls-cicd (任意のアプリ名を入力) Serverless: What deployment profile do you want to use? default (デプロイ用プロファイルを選択) Your project is setup for monitoring, troubleshooting and testing Deploy your project and monitor, troubleshoot and test it: - Run “serverless deploy” to deploy your service. - Run “serverless dashboard” to view the dashboard.
orgには、先程作成したorg名もしくはデフォルトのorg名を入力してください。appは任意のアプリ名でOKです。
ダッシュボードを開くとアプリケーションが追加されています。
Lambda + API Gateway + DynamoDBを作成
今回はAPI Gateway + Lambda + DynamoDBのシンプルなAPIを1本だけ用意しました。
org: experiment app: sample-sls-cicd service: name: sample-sls-cicd provider: name: aws region: ap-northeast-1 stage: ${opt:stage, self:custom.defaultStage} profile: ${self:custom.profiles.${opt:stage, self:custom.defaultStage}} runtime: python3.8 stackName: sample-sls-cicd apiName: sample-sls-cicd logRetentionInDays: 7 versionFunctions: false iamRoleStatements: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - lambda:GetLayerVersion - dynamodb:DescribeTable - dynamodb:Query - dynamodb:Scan - dynamodb:GetItem - dynamodb:PutItem - dynamodb:UpdateItem - dynamodb:DeleteItem Resource: "*" environment: DEFAULT_DATA_LIMIT: "20" ENV: ${self:custom.environments.ENV} custom: defaultStage: dev profiles: dev: dev-profile stg: stg-profile prd: prd-profile pythonRequirements: usePipenv: true layer: true environments: ${file(./config/config.${opt:stage, self:custom.defaultStage}.yml)} package: exclude: - .git/** - .venv/** - .pytest_cache/** - config/** - tests - README.md functions: ListItems: name: list_items handler: src/handlers/list_items.handler description: "商品一覧" events: - http: path: /items method: get cors: true resources: Resources: ItemTable: Type: "AWS::DynamoDB::Table" Properties: TableName: Items AttributeDefinitions: - { AttributeName: item_id, AttributeType: S } KeySchema: - { AttributeName: item_id, KeyType: HASH } BillingMode: PAY_PER_REQUEST
Lambdaを用意
テスト実行のため、商品一覧を返す簡易的なLambdaを用意しておきます。
import json import logging import os import boto3 logger = logging.getLogger() logger.setLevel('DEBUG') def list_items(limit, last_key=None): logging.debug(limit) dynamodb = boto3.resource('dynamodb') TABLE_NAME = 'Items' table = dynamodb.Table(TABLE_NAME) scan_kwargs = { 'ConsistentRead': True, 'Limit': limit } if last_key: scan_kwargs['ExclusiveStartKey'] = last_key response = table.scan(**scan_kwargs) logging.info(response) items = response.get('Items', []) return items def handler(event, context): try: logging.info(event) logging.info(context) result = list_items(int(os.environ['DEFAULT_DATA_LIMIT'])) logging.debug(result) return { 'statusCode': 200, # ensure_ascii: 日本語文字化け対応 'body': json.dumps(result, ensure_ascii=False) } except Exception as e: logging.error(e)
CICDの設定
Githubリポジトリに接続
対象のappのメニューからsettings
を開きます。
ci/cd
→connect git
でGithubの方を選択して接続します。
デプロイ対象のリポジトリを選択して、接続できました。
AWSアカウントに接続
次にデプロイ先のAWSアカウントに対象のロールを作成します。
connect
を押すと、IAMロールを作成するCfnのスタック作成画面に遷移します。(connectの右の↓を押すと作成済みのロールを指定することもできます)
任意のスタック名とロール名に変更して、IAMの作成にチェックを入れたら、スタックの作成を押します。
注: スタックがus-east-1に作成されます。
IAMロールが作成されたら無事にAWSにも接続できました。
slackへの通知を設定
CICDの画面上からslackへの通知も設定できます。
add notification
からslackを選択
slack側でserverlessアプリを許可して接続したら、通知タイミングとチャンネルを選ぶだけで通知を飛ばせるようになります。
任意でメンションも飛ばせます。
これで通知まで設定できました。
テストを用意
motoを使ってDynamoをモックするテストを1本用意しておきます。
import src.handlers.list_items as handler from moto import mock_dynamodb2 import boto3 import json import pytest import os ITEMS_TABLE_NAME = 'Items' @mock_dynamodb2() def test_list_items(): ''' 商品一覧の取得結果が一致する ''' dynamodb = boto3.resource('dynamodb') dynamodb.create_table( TableName=ITEMS_TABLE_NAME, KeySchema=[ { 'AttributeName': 'item_id', 'KeyType': 'HASH' } ], AttributeDefinitions=[ { 'AttributeName': 'item_id', 'AttributeType': 'S' }, ], BillingMode='PAY_PER_REQUEST' ) table = dynamodb.Table(ITEMS_TABLE_NAME) data = [ {'item_id': 'item_0001', 'item_name': 'サンプル品1', 'category': '食品'}, {'item_id': 'item_0002', 'item_name': 'サンプル品2', 'category': '食品'}, {'item_id': 'item_0003', 'item_name': 'サンプル品3', 'category': '食品'}, {'item_id': 'item_0004', 'item_name': 'サンプル品4', 'category': '雑貨'}, {'item_id': 'item_0005', 'item_name': 'サンプル品5', 'category': '雑貨'} ] for i in data: table.put_item(TableName=ITEMS_TABLE_NAME, Item=i) # 全件取得 response = handler.list_items(20) assert response == data # 3件取得 response2 = handler.list_items(3) assert len(response2) == 3
デプロイ前にpytestを実行させるため、package.jsonのscriptsを編集します。
{ "name": "sample-serverless-api", "description": "", "version": "0.1.0", "dependencies": {}, "devDependencies": {}, "scripts": { "postinstall": "pip3 install -r requirements.txt", "test": "pytest" } }
ライブラリのインストール時にpipenvを使いましたが、CICD環境で使えるようrequirements.txtも出力しておきます。
$ pipenv lock -r --dev > requirements.txt
ローカルでテストが通ることを確認しておきます。
~/p/s/s/sample-sls-cicd (develop)> pytest =================================================================================================== test session starts =================================================================================================== platform darwin -- Python 3.8.5, pytest-6.0.2, py-1.9.0, pluggy-0.13.1 rootdir: /Users/oka.haruna/playground/sample-sls-cicd collected 1 item tests/unit/test_list_items.py . [100%] ==================================================================================================== 1 passed in 0.75s ====================================================================================================
動作確認
developブランチにcommitしてテストとデプロイが通るか確認します。
statusがsuccessに変わりました!
pytestも実行できています。
slackにも通知が来てました。
AWSコンソールに行ってみると、CFnスタックが作成されて無事にデプロイされています。
試しにデプロイしたLambdaをslsコマンドで動かしてみます。
$ sls invoke -f ListItems { "statusCode": 200, "body": "[]" }
DynamoDBにデータが入っていないので空配列ですが、200レスポンスが返ってきました!
ソースコード
今回のサンプルのソースコードは以下になります。
CM-haruna-oka/sample-sls-cicd - Github
まとめ
以上、Serverless Dashboardを使って簡単にCICD環境が構築することができました。
インフラの構成管理にServerless Frameworkを使っている場合は、非常に簡単にセットアップができて親和性も高くなります。
Serverless FrameworkがNode環境で動く以上、Typescriptの方が相性はいいですが、上記の手順でpytestも実行できることが検証できました!
個人的にいいなと思ったのがAWSへの接続をIAMロールで操作できる点です。他のCI/CDツールだと大体アクセスキーを発行する必要があるのでこれは嬉しいですね。
今回はFreeプランで試したので1人のユーザーしか使えませんが、Teamプラン以上であればサインアップした時のアカウントが最初のOwnerとなり、後からメンバーを追加(任意でOwnerに昇格)していくことでチームでの管理が可能となります。
他の機能も随時検証していきたいと思います!!